﻿using System;
using System.Text;
using ImageEncryptor.Core.Common.Validators;
using System.Security.Cryptography;
using System.Web.Security;
using System.IO;

namespace ImageEncryptor.Core.Security
{
    #region Encryptor
    public class Encryptor : IDisposable
    {
        #region Private fields & constants

        private const int AT_LEAST_ONE_NON_ALFANUMERIC_CHAR = 0X01;
        private const int MAX_PASSWORD_LENGTH = 0X20;
        private const byte MAX_FILENAME_LENGTH = 0X78;
        private const int PLAIN_HEADER_SIZE = 0X80;

        private string _key;
        private byte[] _password;
        private byte[] _salt;
        private ValidatorBase _validatorBase;
        private string _carrierBitmapFilePath;
        private string _hostedBitmapFilePath;

        #endregion

        #region Private methods

        private void SplitKey()
        {
            byte[] convertedKey = Convert.FromBase64String(_key);
            if ((convertedKey.Length < CommonConstants.MIN_REQUIRED_KEY_SIZE) ||
                    convertedKey.Length > CommonConstants.MAX_REQUIRED_KEY_SIZE)
                throw new FormatException("Invalid key length");

            int saltOffset = convertedKey.Length - AesEncryptor.SALT_SIZE;
            _salt = new byte[AesEncryptor.SALT_SIZE];
            _password = new byte[saltOffset];

            Buffer.BlockCopy(convertedKey, 0, _password, 0, _password.Length);
            Buffer.BlockCopy(convertedKey, saltOffset, _salt, 0, _salt.Length);
        }

        private static byte[] GetUnpredictableRandomSalt()
        {
            byte[] salt = new byte[AesEncryptor.SALT_SIZE];
            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
            rng.GetBytes(salt);
            return salt;
        }

        private static string GetSecurePassword(int size = MAX_PASSWORD_LENGTH)
        {
            return Membership.GeneratePassword(size, AT_LEAST_ONE_NON_ALFANUMERIC_CHAR);
        }

        #endregion

        #region Constructors

        public Encryptor(string carrierBitmapFilePath, string hostedBitmapFilePath,
            string key)
        {
            _validatorBase = new ValidatorBase();

            _validatorBase.BreakIfAnyArgIsNull<string>(carrierBitmapFilePath,
                hostedBitmapFilePath, key);

            _carrierBitmapFilePath = carrierBitmapFilePath;
            _hostedBitmapFilePath = hostedBitmapFilePath;
            _key = key;
        }

        #endregion

        #region Public methods

        public static string GetDistributableKey()
        {
            byte[] salt = GetUnpredictableRandomSalt();
            byte[] password = Encoding.ASCII.GetBytes(GetSecurePassword());
            byte[] data = new byte[salt.Length + password.Length];

            int saltOffset = data.Length - password.Length;

            Buffer.BlockCopy(password, 0, data, 0, password.Length);
            Buffer.BlockCopy(salt, 0, data, saltOffset, salt.Length);
            
            return Convert.ToBase64String(data);
        }

        public void DoJob()
        {
            // Get hosted file data
            byte[] fileData = File.ReadAllBytes(_hostedBitmapFilePath);
            string fileName = new FileInfo(_hostedBitmapFilePath).Name;
            byte fileNameLength = (byte) Math.Min(MAX_FILENAME_LENGTH, fileName.Length);

            // Split the key in password & salt
            SplitKey();

            // Encrypt data
            byte[] fileDataEncrypted = AesEncryptor.Encrypt(fileData,
                _password, _salt);

            // Generate header
            byte[] header = new byte[PLAIN_HEADER_SIZE];

            // First 4 bytes are for the file length
            header[0] = (byte)(fileDataEncrypted.Length >> 24);
            header[1] = (byte)((fileDataEncrypted.Length >> 16) & 0xFF);
            header[2] = (byte)((fileDataEncrypted.Length >> 8) & 0xFF);
            header[3] = (byte)(fileDataEncrypted.Length & 0xFF);

            // Next byte is for the file name length
            header[4] = fileNameLength;

            // Last 3 bytes for the signature code
            header[header.Length - 3] = (byte)(CommonConstants.SIGNATURE[0]);
            header[header.Length - 2] = (byte)(CommonConstants.SIGNATURE[1]);
            header[header.Length - 1] = (byte)(CommonConstants.SIGNATURE[2]);

           
            // Put file name
            for (int i = 0; i < fileNameLength; i++)
            {
                header[5 + i] = Convert.ToByte(fileName[i]);
            }

            // Encrypt header
            byte[] headerEncrypted = AesEncryptor.Encrypt(header, _password, _salt);

            // Append encrypted data to carrier bitmap
            using (var fileStream = new FileStream(_carrierBitmapFilePath, FileMode.Append))
            {
                fileStream.Write(fileDataEncrypted, 0, fileDataEncrypted.Length);
                fileStream.Write(headerEncrypted, 0, headerEncrypted.Length);
            }
        }

        #endregion

        #region Implementations

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                _password = null;
                _key = null;
                _validatorBase = null;
            }

        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion
    }
    #endregion
}
